Анализ данных "hh_database". Часть вторая.¶

  • Загрузка и подготовка данных к разведывательному анализу
  • Исследование зависимостей в данных
  • Визуальный анализ
  • Очистка данных

Рабочий черновик (draftbook)

In [1]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import plotly.graph_objects as go
import os

Загрузка датасета¶

In [2]:
# Локальная папка с датасетами, если есть
!ls data
ExchangeRates.zip
dst-3.0_16_1_hh_database.zip
hh_data.zip
In [3]:
# Загрузка данных в зависимости от среды выполнения (vscode, jupiter notebook, collab.net)

file_id = '16I38xoQDFIQIAQwYPW60M53xaBsdqSzI'
google_drive_url='https://drive.google.com/uc?export=download&confirm=no_antivirus&id='

local_file  = './data/hh_data.zip'
remote_url  = google_drive_url + file_id

# есть локальный - берем локальный, нет - тянем с гугл диска
if os.path.exists('./data') and os.path.exists(local_file):
    print('Load local:', local_file)
    url = local_file
else:
    print('Load remote:', remote_url)
    url = remote_url

# Ускорим перезагрузку если уже "стянули"
hh_df : pd.DataFrame
if 'hh_df' in globals():
    hh_data = hh_df.copy()
    print('Reload copy from memory')
else:
    hh_df = pd.read_csv(url, compression='zip', sep=';')
    hh_data = hh_df.copy()
    print('Loaded')
Load local: ./data/hh_data.zip
Loaded

Вывод общей информации¶

In [4]:
# Информация по столбцам
hh_data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 44744 entries, 0 to 44743
Data columns (total 23 columns):
 #   Column                           Non-Null Count  Dtype  
---  ------                           --------------  -----  
 0   Ищет работу на должность:        44744 non-null  object 
 1   Последнее/нынешнее место работы  44743 non-null  object 
 2   Последняя/нынешняя должность     44742 non-null  object 
 3   Обновление резюме                44744 non-null  object 
 4   Авто                             44744 non-null  object 
 5   Образование                      44744 non-null  object 
 6   Пол                              44744 non-null  object 
 7   Возраст                          44744 non-null  int64  
 8   Опыт работы (месяц)              44574 non-null  float64
 9   Город                            44744 non-null  object 
 10  Готовность к переезду            44744 non-null  bool   
 11  Готовность к командировкам       44744 non-null  bool   
 12  проектная работа                 44744 non-null  bool   
 13  волонтерство                     44744 non-null  bool   
 14  полная занятость                 44744 non-null  bool   
 15  частичная занятость              44744 non-null  bool   
 16  стажировка                       44744 non-null  bool   
 17  сменный график                   44744 non-null  bool   
 18  полный день                      44744 non-null  bool   
 19  гибкий график                    44744 non-null  bool   
 20  вахтовый метод                   44744 non-null  bool   
 21  удаленная работа                 44744 non-null  bool   
 22  ЗП (руб)                         44744 non-null  float64
dtypes: bool(12), float64(2), int64(1), object(8)
memory usage: 4.3+ MB
In [5]:
# Размер, пропуски и статистика
print('Row, Col:', hh_data.shape)
print('NaN`s   Column')
for colm in hh_data.columns:
    cnt_na = hh_data[colm].isna().sum()
    if cnt_na:  print(f'{cnt_na:5}  ', colm)
#
display(hh_data.describe())
display(hh_data.describe(include='object'))
display(hh_data.describe(include='bool'))
Row, Col: (44744, 23)
NaN`s   Column
    1   Последнее/нынешнее место работы
    2   Последняя/нынешняя должность
  170   Опыт работы (месяц)
Возраст Опыт работы (месяц) ЗП (руб)
count 44744.000000 44574.000000 4.474400e+04
mean 32.196741 114.418944 7.652146e+04
std 7.929800 79.047861 1.359203e+05
min 14.000000 1.000000 1.000000e+00
25% 27.000000 57.000000 3.708220e+04
50% 31.000000 100.000000 5.900000e+04
75% 36.000000 154.000000 9.500000e+04
max 100.000000 1188.000000 2.430488e+07
Ищет работу на должность: Последнее/нынешнее место работы Последняя/нынешняя должность Обновление резюме Авто Образование Пол Город
count 44744 44743 44742 44744 44744 44744 44744 44744
unique 14929 30214 16927 18838 2 4 2 4
top Системный администратор Индивидуальное предпринимательство / частная п... Системный администратор 07.05.2019 09:50 Не указано высшее М Москва
freq 3099 935 2062 25 32268 33863 36211 16621
Готовность к переезду Готовность к командировкам проектная работа волонтерство полная занятость частичная занятость стажировка сменный график полный день гибкий график вахтовый метод удаленная работа
count 44744 44744 44744 44744 44744 44744 44744 44744 44744 44744 44744 44744
unique 2 2 2 2 2 2 2 2 2 2 2 2
top False True False False True False False False True False False False
freq 28719 31646 36676 44258 43284 31608 41940 32019 41716 29160 41660 29722

Подготовка данных к анализу¶

In [6]:
#(!) В дату. иначе дубликатов будет меньше для теста
hh_data['Обновление резюме'] = pd.to_datetime(hh_data['Обновление резюме']).dt.date
# Переведем в категориальный тип признаки, с которыми будем работать
hh_data['Образование'] = hh_data['Образование'].astype('category')
hh_data['Город'] = hh_data['Город'].astype('category')
hh_data['Ищет работу на должность:'] =  hh_data['Ищет работу на должность:'].astype('category')

Визуальный анализ¶

Распределение признака «Возраст»¶

Постройте распределение признака «Возраст». Опишите распределение, отвечая на следующие вопросы:

  • Чему равна мода распределения?
  • Каковы предельные значения признака, в каком примерном интервале находится возраст большинства соискателей?
  • Есть ли аномалии для данного признака? Если есть, то какие значения вы бы причислили к таковым?

к заданию 4.1

In [7]:
fig = px.histogram( data_frame=hh_data, 
    height=540, # width=700, 
    title='Распределение возраста соискателей',
    x='Возраст',
    #nbins=100,
    marginal='box',
    #histnorm='percent',
).update_layout(bargap=0.05)
fig.show()

Вывод

  • Мода распределения - 30 лет
  • Предельные значения возраста соискателей. минимум - 14 лет, максимум - 100.
  • Основной интервал возраста большинства соискателей от 14 до 49 лет
  • Значение в 100 лет явная аномалия.
  • Значения свыше 49 лет можно отнести к потенциальным выбросам.
In [8]:
# Информация для контроля 
# print('Мода:',hh_data['Возраст'].mode()[0])
# hh_data[['Возраст']].describe()

Распределение признака "Опыт работы (месяц)"¶

Постройте распределение признака «Опыт работы (месяц)». Опишите распределение, отвечая на следующие вопросы:

  • Чему равна мода распределения?
  • Каковы предельные значения признака, в каком примерном интервале находится опыт работы большинства соискателей?
  • Есть ли аномалии для признака? Если есть, то какие значения вы бы причислили к таковым?

к заданию 4.2

In [9]:
fig = px.histogram( data_frame=hh_data, height=540, # width=700, 
    title='Распределение опыта работы соскателя (месяцы)',
    x='Опыт работы (месяц)',
    #nbins=100,
    marginal='box',
    #histnorm='percent',
)#.update_layout(bargap=0.05)
fig.update_layout(bargap=0.05)
fig.show()

Вывод

  • Мода распределения (по графику) - 80-84 месяца, расчетная - 81 месяц
  • Предельные значения опыта работы соискателей. минимум - 1 месяц, максимум - 1188.
  • Основной интервал опыта работы большинства соискателей от 1 до 299 месяцев
  • Значение в 1188 месяцев является явной аномалией.
In [10]:
# Информация для контроля
# print('Мода:', hh_data['Опыт работы (месяц)'].mode()[0])
# display(hh_data[['Опыт работы (месяц)']].describe())

Распределение признака «ЗП (руб)»¶

Постройте распределение признака «ЗП (руб)». Опишите распределение, отвечая на следующие вопросы:

  • Чему равна мода распределения?
  • Каковы предельные значения признака, в каком примерном интервале находится заработная плата большинства соискателей?
  • Есть ли аномалии для признака заработной платы? Если есть, то какие значения вы бы причислили к таковым?

к заданию 4.3

In [11]:
# для лучшей визуализации немного купируем, отбросим уж совсем явные аномалии более миллиона
# в интерактивном варианте оставим как есть
# mask = hh_data['ЗП (руб)'] <= 1E6 
fig = px.histogram( data_frame=hh_data , # hh_data[mask]
    height=540, # width=700, 
    title='Распределение желаемой заработной платы соискателя (руб)',
    x='ЗП (руб)',
    marginal='box',
    #nbins=100,
    #histnorm='percent',
).update_layout(bargap=0.05)
fig.show()

# del mask

Вывод

  • Мода распределения (по графику) - 50-52 тыс.руб.
  • Предельные значения желаемой заработной платы соискателей: минимум - 1 тыс.руб, максимум - 24 мил.руб.
  • Основной интервал желаемой заработной платы большинства соискателей от 1 до 180 тыс.руб
  • Значение свыще 1 мил.руб можно отнести к явным аномалиям
In [12]:
# Информация для контроля
# print('Мода:', hh_data[mask]['ЗП (руб)'].mode()[0])
# display(hh_data[mask][['ЗП (руб)']].describe())
# hh_data[hh_data['ЗП (руб)'] > 1E6]['ЗП (руб)']

Зависимость медианной «ЗП (руб)» от уровня «Образование»¶

Постройте диаграмму, которая показывает зависимость медианной желаемой заработной платы («ЗП (руб)») от уровня образования («Образование»).

Используйте для диаграммы данные о резюме, где желаемая заработная плата меньше 1 миллиона рублей.

Сделайте выводы по представленной диаграмме:

  • Для каких уровней образования наблюдаются наибольшие и наименьшие уровни желаемой заработной платы?
  • Как вы считаете, важен ли признак уровня образования при прогнозировании заработной платы?

к заданию 4.4

In [13]:
mask = hh_data['ЗП (руб)'] < 1e6
#display(hh_data[mask].groupby('Образование')[['ЗП (руб)']].median())

fig = px.bar(
    data_frame=hh_data[mask].groupby('Образование')[['ЗП (руб)']].median().reset_index(),
    title='Зависимость медианной желаемой заработной платы от уровня образования',
    x='Образование',
    y='ЗП (руб)',
    color='Образование'
)
fig.show()

del mask

Вывод

  • Наибольшее медианное значение желаемой заработной платы находится в категории "высшее", следом идет "неоконченное высшее". Наименьший показатель разделяют между собой категории "среднее специальное" и "среднее"
  • Признак "Образование" является достаточно весомым при прогнозировании заработной платы
In [14]:
# Информация для контроля
# hh_data[hh_data['ЗП (руб)'] < 1e6].groupby('Образование')[['ЗП (руб)']].median()

Распределение «ЗП (руб)» в зависимости от «Город»¶

Постройте диаграмму, которая показывает распределение желаемой заработной платы («ЗП (руб)») в зависимости от города («Город»). Используйте для диаграммы данные о резюме, где желаемая заработная плата меньше 1 миллиона рублей.

Сделайте выводы по полученной диаграмме:

  • Как соотносятся медианные уровни желаемой заработной платы и их размах в городах?
  • Как вы считаете, важен ли признак города при прогнозировании заработной платы?

к заданию 4.5

In [15]:
mask = hh_data['ЗП (руб)'] < 1e6
fig = px.box(
    data_frame=hh_data[mask],
    title='Зависимость медианной желаемой заработной платы от места проживания',
    x='ЗП (руб)',
    y='Город',
    color='Город'
)
fig.show()

del mask

Вывод

  • По всем городам с увеличением медианы желаемой заработной платы растет и ее размах в сторону максимума.
  • Признак "Город" соискателя является крайне важным при прогнозировании заработной платы
In [16]:
# Информация для контроля

Зависимость медианной «ЗП (руб)» от признаков «Готовность»¶

Постройте многоуровневую столбчатую диаграмму, которая показывает зависимость медианной заработной платы («ЗП (руб)») от признаков «Готовность к переезду» и «Готовность к командировкам».

Проанализируйте график, сравнив уровень заработной платы по категориям.

к заданию 4.6

In [17]:
# сгруппируем и локализуем значения признаков
group_by = hh_data.groupby(['Готовность к переезду','Готовность к командировкам'])['ЗП (руб)'].median().reset_index()
group_by['Готовность к переезду'].replace({True:'Да', False:'Нет'}, inplace=True)
group_by['Готовность к командировкам'].replace({True:'Да', False:'Нет'}, inplace=True)
 
fig = px.bar( 
    data_frame=group_by,
    title='Медианный уровень желаемой зароботной платы<br>от готовности к переезду и командировкам',
    x='Готовность к командировкам',
    y='ЗП (руб)',
    color='Готовность к переезду',
    barmode='group',
)
#fig.update_xaxes(type='category', categoryorder='category ascending')
fig.show()

del group_by

Вывод

  • Медианный уровень желаемой зароботной платы соискателя растет в зависимости от его готовности к переезду и командировкам.
  • Можно сказать, что определяющим фактором является готовность к командировкам.
In [18]:
# Информация для контроля

Зависимость медианной «ЗП (руб)» от «Возраст» и «Образование»¶

Постройте сводную таблицу, иллюстрирующую зависимость медианной желаемой заработной платы от возраста («Возраст») и образования («Образование»).

По полученной сводной таблице постройте тепловую карту.

Проанализируйте тепловую карту, сравнив показатели внутри групп.

к заданию 4.7

In [19]:
fig = px.imshow( 
    hh_data.pivot_table(
        columns='Возраст',   index='Образование',
        values='ЗП (руб)',   aggfunc=np.median ),
    title="Тепловая карта зависимости медианной желаемой заработной платы от возраста и образования",
    labels={'color':'ЗП (руб)'},
    color_continuous_scale='YlOrRd', # 'Viridis' 'Blues' 'RdBu'
)
fig.show()

Вывод

  • Наибольшая тенденция к увеличению максимальной медианной желаемой заработной платы от возраста наблюдается в группе "высшее" образование с пиком показателя возраст в 41 год. Следом по показателю роста заработной платы от возраста идет группа "неоконченное высшее" с пиком в 43 года
  • В группе "среднее" какой либо явной тенденции увеличения медианной желаемой заработной платы от возраста не наблюдается.
  • В группе "среднее специальное" есть тенденция к увеличению показателя желаемой заработной платы от возраста, но уровень желаемой зарплаты ниже чем в группах "высшее" и "неоконченное высшее"
In [20]:
# Информация для контроля
# Посмотрим на более реалистичную картинку
# mask = (hh_data['Возраст'] > 17) & (hh_data['Возраст'] < 65)

# fig = px.imshow( 
#     hh_data[mask].pivot_table(columns='Возраст', index='Образование', values='ЗП (руб)', aggfunc=np.median ),
#     title="Тепловая карта зависимость медианной желаемой заработной платы от возраста и образования",
#     labels={'color':'ЗП (руб)'},
#     color_continuous_scale='YlOrRd', # 'Viridis' 'Blues' 'RdBu'
# )
# fig.show()

# del mask

Зависимость «Опыт работы (месяц)» от «Возраст»¶

Постройте диаграмму рассеяния, показывающую зависимость опыта работы («Опыт работы (месяц)») от возраста («Возраст»). Опыт работы переведите из месяцев в года, чтобы признаки были в едином масштабе.

Постройте на графике дополнительно прямую, проходящую через точки (0, 0) и (100, 100). Данная прямая соответствует значениям, когда опыт работы равен возрасту человека. Точки, лежащие на этой прямой и выше неё, — аномалии в наших данных (опыт работы больше либо равен возрасту соискателя).

к заданию 4.8

In [21]:
# Добавляем признак "Опыт работы" в годах и рисуем
hh_data['Опыт работы'] = (hh_data['Опыт работы (месяц)'] /12).round(1)

fig = px.scatter( data_frame=hh_data.assign(_vsize=1),
    height=740, # width=820,
    title='Зависимость опыта работы от возраста',
    x='Возраст',  y='Опыт работы',  color='Образование',
    opacity=0.5,  size='_vsize', size_max=8,
    hover_data={'_vsize':False},
    range_x=[-5, 105],  range_y=[-5, 105],
)

# Визуализируем "Горцев"
fig.add_trace( go.Scatter(x=[-5,105], y=[-5,105], mode='lines', name='Опыт работы равен возрасту',
               line = {'color':'red','width':1.5})
)

# Визуализируем "Работников с горшка"
fig.add_trace( go.Scatter(x=[11,121], y=[-5,105], mode='lines', name='Опыт работы (Возраст-16)',
               line = {'color':'orange','width':1.5})
)
fig.show()

#(!) Обязательно удаляем доп. признаки для анализа, иначе кол-во дубликатов и т.п. 
# будут не соответствовать тестовым значениям. Или используем df.assign(...) для графиков
hh_data.drop(columns='Опыт работы', inplace=True)

Вывод

  • Присутствует видимая зависимость роста опыта работы от возраста.
  • Наблюдается явный "мусор" в признаке "Опыт работы", превышающий реальный возможный стаж трудовой деятельности ("Возраст"-16, оранжевая линия), что говорит о завышении опыта работы соискателями.
  • Отдельные показатели опыта работы (красная линия) превышают даже возраст соискателя.
In [22]:
# Информация для контроля

Дополнительно*. Распределение показателя пассионарности и мобильности соскателей¶

  • Расмотрим как влияет возраст соискателей на готовность к переезду и коммандировкам

дополнительное задание 4.9

In [23]:
# Не будем пытать уж совсем старенького дедушку
mask_age = hh_data['Возраст'] < 100 

# Сформируем признак-категорию готовности
def mobile_alacrity_predict(row:pd.Series)->str:
    if row['Готовность к переезду'] and row['Готовность к командировкам']:
        return 'К переезду и командировкам'
    elif not row['Готовность к переезду'] and row['Готовность к командировкам']:
        return 'Только к переезду'
    elif row['Готовность к переезду'] and not row['Готовность к командировкам']:
        return 'Только к командировкам'
    else:
        return 'Отсутствует'

hh_data['Готовность'] = hh_data.apply(mobile_alacrity_predict, axis=1)

fig = px.histogram( data_frame= hh_data[mask_age], 
    height=740, # width=700, 
    title='Распределение возраста соскателей по группам<br>готовых к смене места жительства и командировкам',
    x='Возраст',
    color='Готовность',
    marginal='box',
    barmode='overlay',
).update_layout(bargap=0.05)
fig.show()

del mask_age
#(!) Обязательно удаляем доп. признаки для анализа, иначе кол-во дубликатов и т.п. 
# будут не соответствовать тестовым значениям. Или используем df.assign(...) для графиков
hh_data.drop(columns='Готовность', inplace=True)

Вывод

  • В целом распределения перекрываются. Распределение сооскателей, готовых к переезду, немного смещенно к более старшей возрастной группе.
  • Пик (мода) соискателей, готовых к переезду приходиться на 30 лет. Моды групп не готовых к перезду приходится на 24 года.
  • Если до 24 лет (студенты) приобладает возрастная группа "домоседов", то в последующем количество готовых к переезду и готовых и к переезду и к командировками превышает остальные группы.
  • Группа готовых только на командировки значительно меньше остальных на всем распределении возраста соискателей.
  • К 50 годам количество готовых на переезды с командировками и количество не готовых к этому примерно уравнивается. А вот тех кто готов к переезду для смены работы, но не желает жить командировками остается стабильно больше до 60 лет.

Дополнительно*. Распределение Топ10 ожидаемой медианной зарплаты из Топ25 самых популярных категориях искомых должностей¶

  • Рассмотрим как распределяется Топ10 самых ожидаемых зарпат из Топ25 самых популярных категориях искомых должностей

дополнительное задание 4.10

In [24]:
# Ограничемся реалистичной минимальной зарплатой в 12т (близко к МРОТ)
# и более реалистичными зарплатами в максимуме. Топ менеджеры вряд ли ищут работу через hh.ru
mask_salary_bw = (hh_data['ЗП (руб)'] > 12_000) & (hh_data['ЗП (руб)'] < 350_000) 

# Отберем в категории топ 25 искомых должностей по количеству соскателей
top25_jobs = hh_data.groupby('Ищет работу на должность:')['ЗП (руб)'] \
                .agg(['count', 'median']).nlargest(25, columns='count')

# Выберем из них топ 10 самых больших ожидаемых медианных зарплат
top10_salary = top25_jobs.nlargest(10, columns='median').index

# display(top25_jobs.nlargest(10, columns='median'))
# Аналитики лидирую по количеству, руководители проектов по з/п. И это топ 10. Кризис, однако.

#отбираем и рисуем
top10_salary_mask = hh_data['Ищет работу на должность:'].isin(top10_salary)
fig = px.box(
    height=720, # width=700, 
    data_frame=hh_data[mask_salary_bw & top10_salary_mask],
    title='Распределение желаемой заработной платы в топ 10 ожидаемых<br>медианных зарплат из топ 25 искомых должностей',
    x='ЗП (руб)',
    y='Ищет работу на должность:',
    color='Ищет работу на должность:'
).update_layout(showlegend=False)
fig.show()

del mask_salary_bw, top25_jobs, top10_salary, top10_salary_mask

Вывод

  • В целом распределения ожидаемой з/п более-менее реалистичные. В сторону более максимальной з/п ожидаемо выделяются группы "Руководителей проекта/проектов". "Менеджеры проектов" это скорее всего соискатели из IT-сферы и т.п., а "Руководители" это более широкий спектр направлений.
  • По IT-сфере несколько заниженна ожидаемая з/п у групп "Инженер-программист" и "Программист". Похоже на наименование должностей системных администраторов в бюджетной сфере и тогда соответствует действительности.
  • Можно отметить, что в группе "Програмист-разработчик" несколько завышенная ожидаемая з/п относительно групп "Frontend-разработчик" и "Аналитик".

Очистка данных¶

Удаление дубликатов. Копия датасета для очистки¶

Начнём с дубликатов в наших данных. Найдите полные дубликаты в таблице с резюме и удалите их.

к заданию 5.1

In [25]:
#(!) Далее работаем с копией данных / перезагружать с этого места
hh_cleaned = hh_data.copy()

# Ищем дубликаты, выводим их количество
hh_duplicates = hh_cleaned[hh_cleaned.duplicated()]
print('Кол-во полных дубликатов:', hh_duplicates.shape[0])

# и удаляем. смотрим что осталось. (44744-161)
hh_cleaned.drop_duplicates(inplace=True)
print('Результирующее число записей:', hh_cleaned.shape[0])
Кол-во полных дубликатов: 161
Результирующее число записей: 44583
In [26]:
# Вариант с уникальными признаками (у нас нет)
#list_columns = list(hh_data.columns)
#list_columns.remove([,..]) # remove calc unq columns
#mask = hh_data.duplicated(subset=list_columns)

Работа с пропусками¶

Итак, у нас есть пропуски в трёх столбцах: «Опыт работы (месяц)», «Последнее/нынешнее место работы», «Последняя/нынешняя должность». Поступим следующим образом:

  • Удалите строки, где есть пропуск в столбцах с местом работы и должностью.
  • Пропуски в столбце с опытом работы заполните медианным значением.

к заданиям 5.2, 5.3

In [27]:
print('Признаки с пропусками:')
print('NaN`s   Column')
for colm in hh_cleaned.columns:
    cnt_na = hh_cleaned[colm].isna().sum()
    if cnt_na:  print(f'{cnt_na:5}  ', colm)
Признаки с пропусками:
NaN`s   Column
    1   Последнее/нынешнее место работы
    2   Последняя/нынешняя должность
  168   Опыт работы (месяц)
In [28]:
# удалим столбцы с малым кол-вом пропусков
subset = ['Последнее/нынешнее место работы', 'Последняя/нынешняя должность']
hh_cleaned.dropna(subset=subset, inplace=True)  


# заполняем медианым значением признак со значительными пропусками
hh_cleaned.fillna({'Опыт работы (месяц)': hh_cleaned['Опыт работы (месяц)'].median()}, inplace=True)
# Проверяем
print('Кол-во пропусков "Опыт работы":', hh_cleaned['Опыт работы (месяц)'].isna().sum())
# И в целом
print('Результирующее число записей:', hh_cleaned.shape[0])
print('Общее кол-во пропусков:',hh_cleaned.isna().sum().sum())
print('Среднее "Опыт работы (месяц)":', hh_cleaned['Опыт работы (месяц)'].mean())
Кол-во пропусков "Опыт работы": 0
Результирующее число записей: 44581
Общее кол-во пропусков: 0
Среднее "Опыт работы (месяц)": 114.35777573405711

Ликвидация выбросов¶

  • Удалите резюме, в которых указана заработная плата либо выше 1 миллиона рублей, либо ниже 1 тысячи рублей

  • В процессе разведывательного анализа мы обнаружили резюме, в которых опыт работы в годах превышал возраст соискателя. Найдите такие резюме и удалите их из данных.

  • Найдите выбросы с помощью метода z-отклонения и удалите их из данных, используйте логарифмический масштаб.

    Давайте сделаем "послабление" на 1 сигму (возьмите 4 сигмы) в правую сторону. Выведите таблицу с полученными выбросами и оцените, с каким возрастом соискатели попадают под категорию выбросов?

  • В какую сторону асимметрично логарифмическое распределение? Напишите об этом в комментарии к графику.

к заданиям 5.4, 5.5, 5.6

In [29]:
# Отфильтруем, оценим и удалим
mask_not_bw_1k_1m = (hh_cleaned['ЗП (руб)'] < 1E3) | (hh_cleaned['ЗП (руб)'] > 1E6)

print('Результирующее число записей до очистки:', hh_cleaned.shape[0])
print('Кол-во выбросов в "ЗП" менее 1K и более 1M:', hh_cleaned[mask_not_bw_1k_1m].shape[0])

hh_cleaned.drop(index=hh_cleaned[mask_not_bw_1k_1m].index, inplace=True)
del mask_not_bw_1k_1m
print('Результирующее число записей после очистки:', hh_cleaned.shape[0])
Результирующее число записей до очистки: 44581
Кол-во выбросов в "ЗП" менее 1K и более 1M: 89
Результирующее число записей после очистки: 44492
In [30]:
# Ищем и удаляем супер работяг "год за два" в режиме 24/7
# Можно было бы уменьшить опыт до (Возраст-16)*12, но доктор сказал - резать.
mask_work_over_age = hh_cleaned['Опыт работы (месяц)'] > hh_cleaned['Возраст']*12

print('Результирующее число записей до очистки:', hh_cleaned.shape[0])
print('Кол-во выбросов с превышением опыта работы:', hh_cleaned[mask_work_over_age].shape[0])

hh_cleaned.drop(index=hh_cleaned[mask_work_over_age].index, inplace=True)
del mask_work_over_age
print('Результирующее число записей после очистки:', hh_cleaned.shape[0])
Результирующее число записей до очистки: 44492
Кол-во выбросов с превышением опыта работы: 7
Результирующее число записей после очистки: 44485
In [31]:
# Очистка от выбросов в признаке "Возраст" методом трех сигм
# Сперва посмотрим распределение на графике в логарифмическом маштабе

hh_cleaned['Возраст (лог)'] = np.log(hh_cleaned['Возраст'])
# Заготовочка с курса и в право 4 сигмы
mu = hh_cleaned['Возраст (лог)'].mean()
sigma = hh_cleaned['Возраст (лог)'].std()
left_shift  = 3
right_shift = 4
lower_bound = mu - left_shift  * sigma 
upper_bound = mu + right_shift * sigma

fig = px.histogram( data_frame=hh_cleaned, 
    height=540, # width=700, 
    title='Распределение возраста соискателей (логарифмический маштаб)',
    x='Возраст (лог)',
    marginal='box',
    range_y=[0, 3000],
).update_layout(bargap=0.05)

# (!) Для отображения результата на графике наведите курсор в нижную часть линии

fig.add_trace( go.Scatter(x=[mu,mu], y=[0,3000], mode='lines', name='Mean',
    line = {'color':'red','width':2}))
fig.add_trace( go.Scatter(x=[lower_bound, lower_bound], y=[0,3000], mode='lines', name='Lower bound',
    line = {'color':'green','width':2}))
fig.add_trace( go.Scatter(x=[upper_bound, upper_bound], y=[0,3000], mode='lines', name='Upper bound',
    line = {'color':'orange','width':2}))

fig.show()
In [32]:
# Выводим список значений выбросов
mask_log_age_emissions = (hh_cleaned['Возраст (лог)'] < lower_bound) | (hh_cleaned['Возраст (лог)'] > upper_bound)
print('Кол-во выбросов "Возраст (лог)":', mask_log_age_emissions.sum())
display(hh_cleaned[mask_log_age_emissions][['Возраст (лог)','Возраст']])

print('Нижняя граница (срд-3*сигмы):', round(np.exp(lower_bound)))
print('Верхняя граница (срд+4*сигмы):', round(np.exp(upper_bound)))


# Удостоверимся что лог. распределение лево-симмитричное, как на графике
print('Кооф. асимметрии:', round(hh_cleaned['Возраст (лог)'].skew(), 2))

#Удаляем выбросы
hh_cleaned.drop(index=hh_cleaned[mask_log_age_emissions].index, inplace=True)

del mask_log_age_emissions
hh_cleaned.drop(columns='Возраст (лог)', inplace=True)
Кол-во выбросов "Возраст (лог)": 3
Возраст (лог) Возраст
31137 2.70805 15
32950 2.70805 15
33654 4.60517 100
Нижняя граница (срд-3*сигмы): 16
Верхняя граница (срд+4*сигмы): 79
Кооф. асимметрии: 0.45

Вывод

  • Полученые границы возраста для выявления выбросов с использованием метода z-отклонения состовляют - менее 16 и более 79 лет
  • Логарифмическое распределение возраста смещено в лево и является левосторонним.
In [33]:
# Итог очистки
print('Результирующее число записей до очистки:', hh_data.shape[0])
print('Результирующее число записей после очистки:', hh_cleaned.shape[0])
print('Удалено записей:', hh_data.shape[0]  - hh_cleaned.shape[0])
Результирующее число записей до очистки: 44744
Результирующее число записей после очистки: 44482
Удалено записей: 262

Сохранение очищенного датасета¶

In [34]:
# Сохраняем, если присутствует локальный ./data
if  os.path.exists('./data'):
    if os.path.exists('./data/hh_cleaned.zip'):
        os.remove('./data/hh_cleaned.zip')
    hh_cleaned.to_csv('./data/hh_cleaned.zip', index=False, sep=';', compression={'method':'zip','archive_name':'hh_cleaned.csv'})
In [35]:
### Все... Все только начинается. Переносим в пректный ноутбук